<template>
  <div
    class="node"
    v-show="visible"
    @dragenter="dragenter"
    @dragover="dragover"
    @drop="drop"
    @dragleave="dragleave"
    @click.stop="actived"
    @contextmenu.prevent.stop="showContextMenu"
    :class="{ active: isActive, dragentered: dragentered }"
    :style="computedStyle"
  >
    <component
      :is="currPage"
      v-bind="nodeInfo.props"
      :ref="nodeInfo.id"
      :style="componentStyle"
    >
      <template v-if="nodeInfo.child && slots">
        <node
          v-for="(item, index) in nodeInfo.child"
          :slot-style="slots(index)?.style"
          :slot="slots(index)?.name"
          :node-index="index"
          :stacked="nodeInfo.stack"
          :key="item.id"
          :info="item"
        ></node>
      </template>
    </component>
    <selecter
      ref="selector"
      :packed="packed"
      :is-root="isRootNode && !nodeInfo.packed"
      :visible="isActive"
      :warp-style="nodeInfo.style"
    ></selecter>
    <template v-if="nodeInfo.child && !slots">
      <node
        v-for="(item, index) in nodeInfo.child"
        :node-index="index"
        :stacked="nodeInfo.stack"
        :key="item.id"
        :info="item"
      ></node>
    </template>
    <div
      :style="{ width: phoneSize.width }"
      :class="{ hover: dragenteredDivider }"
      class="node-divider"
      @dragleave="dragleaveDivider"
      @dragover="dragoverDivider"
      @dragenter="dragenterDivider"
      @drop="dropDivider"
      v-if="!stacked && !fixed"
    ></div>
    <div
      v-show="!stacked && !fixed"
      class="type-icon iconfont icon-stack"
    ></div>
    <div v-show="fixed" class="type-icon iconfont icon-nail-fixed"></div>
    <div
      v-show="nodeInfo.leaf"
      :style="{ 'margin-right': !stacked || fixed ? '14px' : '0' }"
      class="type-icon iconfont icon-leaf"
    ></div>
  </div>
</template>

<script type="text/ecmascript-6">
import cLoader from 'src/extend/componentLoader'
import BaseComponent from 'src/extend/BaseComponent'
import BaseNode from 'src/extend/BaseNode'
import Selecter from './Selecter'
import cloneDeep from 'lodash/cloneDeep'
import Util from 'src/extend/Util'
import { modifyNodeId, toSafeNumber, componentAddJudge, confirmWithGoodbye } from '../assets/js/common'

export default {
  mixins: [BaseNode, BaseComponent],
  name: 'node',
  components: { Selecter },
  props: {
    info: {
      required: true,
      type: Object
    },
    stacked: {
      type: Boolean,
      default: true
    },
    nodeIndex: {
      type: Number,
    },
    slotStyle: {
      type: Object,
      default: null
    }
  },
  data: () => ({
    dragenteredDivider: false,
    componentEvents: 'auto',
    oldId: '',
    isActive: false,
    dragentered: false,
    mouseovered: false,
    currPage: '',
    slots: null
  }),
  watch: {
    isActive(val) {
      this.componentEvents = val ? 'auto' : 'none'
    },
    'nodeInfo.type': {
      handler(newVal) {
        this.nodeInfo.props = {}
        if (newVal !== 'node') {
          if (newVal === 'truckPageContainer') {
            this.nodeInfo.style = { width: '100%', height: '100%', left: '0', top: '0', position: 'absolute' }
          }
          this.doLoad()
        } else {
          this.currPage = ''
        }
      }
    },
    'nodeInfo.version': 'doLoad',
    stacked: {
      handler(stacked) {
        const styleShot = this.nodeInfo.style
        const bakedStyle = this.nodeInfo.bakedStyle
        if (styleShot.position === 'fixed') return
        if (!['fixed', 'relative', 'absolute'].includes(styleShot.position)) {
          this.nodeInfo.style = { ...styleShot, position: 'relative', left: '0px', top: '0px', bottom: null, right: null }
          return
        }
        if (arguments.length === 1 && stacked) return
        const positionTypes = stacked ? ['absolute', 'relative'] : ['relative', 'absolute']
        if (styleShot.position !== positionTypes[0]) {
          this.nodeInfo.style = Object.assign({}, this.nodeInfo.style, bakedStyle?.position === positionTypes[0] ? bakedStyle : {
            position: positionTypes[0],
            left: '0px',
            top: '0px',
            bottom: null,
            right: null,
          })
          this.nodeInfo.bakedStyle = { ...styleShot, position: styleShot.position }
        }
      },
      immediate: false
    }
  },
  computed: {
    nodeInfo() {
      return { ...this.info, version: this.info.version || '0.1.0' }
    },
    phoneSize() {
      return this.$store.state.setting.phoneSize
    },
    componentStyle() {
      return { 'pointer-events': this.packedChild ? 'none' : this.componentEvents }
    },
    visible() {
      return this.nodeInfo.visible ?? false
    },
    packedChild() {
      return this.isRootNode ? false : this.parentNodeVm.packed || this.parentNodeVm.packedChild
    },
    packed() {
      return !this.isRootNode && this.nodeInfo.packed
    },
    isRootNode() {
      return this.nodeIndex === undefined
    }
  },
  beforeDestroy() {
    delete window.$_nodecomponents[this.oldId]
  },
  mounted() {
    this.oldId = this.nodeInfo.id
    this.initBaseCss()
    this.bindSelectOne()
    this.bindCopy()
    this.bindDrag()
    this.bindPageContainerSelect()
    this.bindRunAnimation()
    this.bindReload()
    this.registerNodeToGlobal()
    this.doLoad()
    console.log('mounted', this.nodeInfo.id)
  },
  methods: {
    registerNodeToGlobal() {
      window.$_nodecomponents = window.$_nodecomponents || {}
      window.$_nodecomponents[this.nodeInfo.id] = this
    },
    initBaseCss() {
      // 可以在这里添加基础CSS，如果需要
    },
    showContextMenu(event) {
      this.ema.fire('show.contextMenu', this, event)
    },
    getComponent(id, child) {
      const component = window.$_nodecomponents[id]
      return child ? component?.$refs[id] : component
    },
    bindRunAnimation() {
      this.ema.bind('animate.timeline.move', (time, alltime) => {
        if (this.nodeInfo.animate.length && this.isVisible()) {
          this.setAnimationFrame(time, alltime)
        }
      })
      this.ema.bind('animate.timeline.move.end', () => {
        if (this.nodeInfo.animate.length) {
          this.$el.style.animation = ''
        }
      })
    },
    setAnimationFrame(time, alltime) {
      let currentAnimation = null
      let timeLine = 0
      for (let val of this.nodeInfo.animate) {
        if (val.infinite) {
          timeLine = alltime
          currentAnimation = val
          break
        }
        timeLine += val.duration * val.countNum + val.delay
        if (timeLine >= time) {
          currentAnimation = val
          break
        }
      }

      if (!currentAnimation) return

      const { duration, timingFunction = 'ease', delay, count = 1, type } = currentAnimation
      let animationDelay = timeLine - time - currentAnimation.duration * currentAnimation.countNum
      if (currentAnimation.infinite) {
        animationDelay = -((time - timeLine - currentAnimation.delay)) % currentAnimation.duration
      }
      const animation = `${duration}s ${timingFunction} ${animationDelay}s ${count} normal none paused ${type}`
      this.$el.style.animation = type ? animation : ''
    },
    bindDrag() {
      this.ema.bind('dragenter.one', id => {
        this.dragentered = id === this.nodeInfo.id
      })
    },
    bindSelectOne() {
      this.ema.bind('select.one', (id, keepContextMenu) => {
        if (this.nodeInfo.locked) return
        this.isActive = id === this.nodeInfo.id
        if (this.isActive) {
          if (!keepContextMenu || keepContextMenu instanceof window.Event) {
            this.ema.fire('hide.contextMenu')
          }
          this.mergeProps()
          window.$vue = this
          setTimeout(() => this.ema.fire('select.oneInfo', { vm: this }), 200)
          if (this.parentNodeVm?.nodeInfo?.type.includes('PageContainer')) {
            this.ema.fire('select.truckPageContainer', id)
          }
        } else {
          window.$vue = null
        }
      })
      this.ema.bind('select.noOne', () => {
        this.isActive = false
        window.$vue = null
      })
      this.ema.bind('delete.down', e => {
        if (this.isActive && e.panelName === 'widgetScene') {
          this.$confirm('确认删除该组件?', '提示', {
            confirmButtonText: '确定',
            cancelButtonText: '取消',
            type: 'warning'
          }).then(() => {
            this.ema.fire('move.node', this.nodeInfo.id, null)
            this.ema.fire('select.noOne')
          }).catch(() => {
            this.$message({ type: 'info', message: '已取消' })
          })
        }
      })
    },
    bindCopy() {
      this.ema.bind('node.copy', node => {
        if (!node || !node.id || node.id !== this.nodeInfo.id) return
        this.parentNodeVm.copyChild(this.nodeInfo, {})
      })
    },
    bindPageContainerSelect() {
      if (this.parentNodeVm?.nodeInfo?.type.includes('PageContainer')) {
        this.ema.bind('select.truckPageContainer', id => {
          this.nodeInfo.visible = id === this.nodeInfo.id
        })
      }
    },
    async doLoad() {
      if (this.nodeInfo.type === 'node' && !window.$vue) {
        this.$set(this.info, 'stack', this.info.stack !== false)
        this.ema.fire('select.one', this.nodeInfo.id)
        return
      }
      try {
        const component = await cLoader.load(this.nodeInfo)
        this.slots = this.calcSlots(component, this.nodeInfo.props)
        const type = this.nodeInfo.type
        const label = this.info.label || this.info.id.replace(type, (component?.label || type))
        const stack = typeof this.info.stack === 'boolean' ? this.info.stack : this.slots ? component.stack : component.stack !== false
        const childLimit = typeof this.info.childLimit === 'number' ? this.info.childLimit : toSafeNumber(component.childLimit, 9999)
        const leaf = typeof this.info.leaf === 'boolean' ? this.info.leaf : !!component.leaf || component.childLimit === 0
        const packed = this.info.packed === true

        let style = this.nodeInfo.style
        if (!style || JSON.stringify(style) === '{}') {
          style = { position: 'absolute', width: '80px', height: '80px', left: '0px', top: '0px', ...component.style, ...this.info.forceStyle }
          if (!this.stacked && (!component.style || component.style.position !== 'fixed')) {
            style = { ...style, position: 'relative', left: null, top: null, bottom: null, right: null }
          }
          delete this.nodeInfo.forceStyle
        }

        this.$set(this.info, 'label', label)
        this.$set(this.info, 'stack', stack)
        this.$set(this.info, 'childLimit', childLimit)
        this.$set(this.info, 'leaf', leaf)
        this.$set(this.info, 'packed', packed)
        this.$set(this.info, 'style', style)

        if (!this.isNodeRegisted(this.nodeInfo.id)) {
          this.registerNode(this.nodeInfo)
        }
        this.currPage = this.nodeInfo.id

        await this.$nextTick()
        const id = this.nodeInfo.id
        const $props = this.$refs[id]?.$props
        if ($props) {
          Object.keys(this.nodeInfo.props).forEach(key => {
            if ($props[key] === undefined) {
              delete this.nodeInfo.props[key]
            }
          })
        }

        if (id === 'root' && !window.$vue) {
          this.ema.fire('select.one', this.nodeInfo.id)
        }
        this.bindCalcSlot()
      } catch (error) {
        console.error('Failed to load component:', error)
      }
    },
    bindReload() {
      this.ema.bind('component.reload', id => {
        if (id === this.nodeInfo.id && this.nodeInfo.type !== 'node') {
          this.currPage = ''
          this.registerNode(this.nodeInfo)
          this.currPage = this.nodeInfo.id
          this.$nextTick(this.actived)
        }
      })
    },
    mergeProps() {
      const id = this.nodeInfo.id
      const $props = this.$refs[id]?.$props
      if ($props) {
        Object.keys($props).forEach(key => {
          this.$set(this.nodeInfo.props, key, $props[key])
        })
      }
    },
    mouseover() {},
    mouseenter(e) {
      if (this.packedChild) return
      this.mouseovered = true
      this.componentEvents = 'none'
    },
    mouseout(e) {
      if (this.packedChild) return
      this.mouseovered = false
      this.componentEvents = 'auto'
    },
    actived(keepContextMenu) {
      if (this.packedChild) return
      console.log('actived....', keepContextMenu, this.nodeInfo)
      if (this.nodeInfo.lock) {
        this.$message({ type: 'warning', message: '已被锁定，请先解锁' })
      } else {
        this.ema.fire('select.one', this.nodeInfo.id, keepContextMenu)
      }
    },
    dragenter(ev) {
      ev.stopPropagation()
      this.ema.fire('dragenter.one', this.nodeInfo.id)
    },
    dragleave(ev) {
      ev.stopPropagation()
    },
    dragover(ev) {
      ev.stopPropagation()
      ev.preventDefault()
    },
    dragenterDivider(ev) {
      ev.stopPropagation()
      this.dragenteredDivider = true
      this.ema.fire('dragenter.one', null)
    },
    dragleaveDivider(ev) {
      ev.stopPropagation()
      this.dragenteredDivider = false
    },
    dragoverDivider(ev) {
      ev.stopPropagation()
      ev.preventDefault()
    },
    dropDivider(ev) {
      ev.stopPropagation()
      ev.preventDefault()
      const parentNode = this.parentNodeVm.nodeInfo
      console.log('drop', ev)
      try {
        const componentInfo = JSON.parse(ev.dataTransfer.getData('componentInfo'))
        if (componentInfo.name) {
          const nodeInfo = Util.getBaseNode(componentInfo)
          parentNode.child.splice(this.nodeIndex, 0, nodeInfo)
          this.$nextTick(() => this.ema.fire('select.one', nodeInfo.id))
        }
      } catch (error) {
        console.error('Failed to parse componentInfo:', error)
      }
    },
    dragstart(ev) {
      ev.stopPropagation()
      ev.dataTransfer.effectAllowed = 'move'
      ev.dataTransfer.setData('moveid', this.nodeInfo.id)
      ev.dataTransfer.setData('dragPos', JSON.stringify({ x: ev.offsetX, y: ev.offsetY }))
      ev.dataTransfer.setDragImage(ev.target, ev.offsetX, ev.offsetY)
    },
    dragend(ev) {
      ev.stopPropagation()
    },
    drop(ev) {
      ev.stopPropagation()
      ev.preventDefault()
      this.dragentered = false
      try {
        const judge = componentAddJudge(this.nodeInfo, this)
        if (!judge.can) throw new Error(judge.msg)

        if (!this.nodeInfo.child) {
          this.$set(this.nodeInfo, 'child', [])
        }

        const currNodePos = this.$el.getBoundingClientRect()
        const mousePos = { top: ev.pageY, left: ev.pageX }
        const dragPos = JSON.parse(ev.dataTransfer.getData('dragPos'))

        const targetPos = {
          top: `${mousePos.top - currNodePos.top - dragPos.y}px`,
          left: `${mousePos.left - currNodePos.left - dragPos.x}px`
        }
        const moveId = ev.dataTransfer.getData('moveid')

        if (moveId) {
          if (moveId !== this.nodeInfo.id) {
            this.ema.fire('move.node', moveId, this.nodeInfo, targetPos)
          }
        } else {
          const componentInfo = JSON.parse(ev.dataTransfer.getData('componentInfo'))
          if (componentInfo.name) {
            let nodeInfo = Util.getBaseNode(componentInfo)
            nodeInfo.forceStyle = { ...targetPos }

            if (this.nodeInfo.type.toLowerCase().endsWith('pagecontainer')) {
              nodeInfo.forceStyle = { position: 'absolute', width: '100%', height: '100%', left: '0', top: '0' }
            }

            this.nodeInfo.child.push(nodeInfo)
            this.$nextTick(() => this.ema.fire('select.one', nodeInfo.id))
          }
        }
      } catch (error) {
        console.error('Failed to drop component:', error)
      }
    },
    moveLayer(node, step) {
      const childs = this.nodeInfo.child
      const index = childs.indexOf(node)
      if (index === -1) return

      if (step === Number.MAX_VALUE) {
        step = childs.length - 1 - index
      } else if (step === Number.MIN_VALUE) {
        step = -index
      }

      const targetIndex = index + step
      if (targetIndex < 0 || targetIndex >= childs.length) return

      const info = childs.splice(index, 1)[0]
      childs.splice(targetIndex, 0, { ...info })
    },
    copyChild(node, { isJson, keepPos }) {
      try {
        const judge = componentAddJudge(this.nodeInfo, this)
        if (!judge.can) throw new Error(judge.msg)

        if (!this.nodeInfo.child) {
          this.$set(this.nodeInfo, 'child', [])
        }

        const cloneNodes = isJson ? JSON.parse(node) : cloneDeep(node)
        const childs = this.nodeInfo.child

        for (let cloneNode of Array.isArray(cloneNodes) ? cloneNodes : [cloneNodes]) {
          if (!keepPos) {
            const isAbsolute = cloneNode.style.position === 'absolute'
            if (cloneNode.style.left) {
              const [left, unit] = cloneNode.style.left.match(/^([-?\d.]+)([a-z%]*)$/)
              cloneNode.style.left = `${parseFloat(left) + (isAbsolute ? (unit === '%' ? 2 : 20) : 0)}${unit}`
            }
            if (cloneNode.style.top) {
              const [top, unit] = cloneNode.style.top.match(/^([-?\d.]+)([a-z%]*)$/)
              cloneNode.style.top = `${parseFloat(top) + (isAbsolute ? (unit === '%' ? 2 : 20) : 0)}${unit}`
            }
          }
          cloneNode = modifyNodeId(cloneNode, Object.keys(window.$_nodecomponents || {}))
          childs.push(cloneNode)
        }

        this.$nextTick(() => this.ema.fire('tree.filter'))
        console.log('childs', childs)
      } catch (error) {
        console.error('Failed to copy child:', error)
      }
    },
    copyStyle() {
      window.$copyedStyle = { ...this.nodeInfo.style }
    },
    pasteStyle() {
      if (!window.$copyedStyle) return
      this.nodeInfo.style = { ...this.nodeInfo.style, ...window.$copyedStyle }
    },
    isNodeRegisted(tag) {
      return !!Vue.component(tag)
    },
    async openPacked({ notice = true, scene = 'db_click_node' }) {
      try {
        const valid = notice ? await confirmWithGoodbye('确定要对当前节点「解除封装」吗？', `openPacked_${scene}`).catch(() => false) : true
        if (!valid) {
          console.log('取消解除节点封装')
          return
        }

        this.nodeInfo.packed = false
        this.ema.fire('tree.filter')
      } catch (error) {
        console.error('Failed to open packed:', error)
      }
    },
    async doPack({ notice = true, scene = 'db_click_node' }) {
      try {
        const valid = notice ? await confirmWithGoodbye('确定要对当前节点执行「封装」操作吗？', `doPack_${scene}`).catch(() => false) : true
        if (!valid) {
          console.log('取消执行节点封装')
          return
        }

        this.nodeInfo.packed = true
        this.ema.fire('tree.filter')
      } catch (error) {
        console.error('Failed to do pack:', error)
      }
    },
    bindCalcSlot() {
      const component = Vue.component(this.nodeInfo.id)
      if (!component || typeof component.options?.slots !== 'function') return

      this.$watch('nodeInfo.props', val => {
        this.slots = this.calcSlots(component.options, val)
      }, { deep: true })
    }
  }
}
</script>

<style lang="stylus" rel="stylesheet/stylus" scoped type="text/stylus">
.node {
  position: relative;
  cursor: pointer;
  user-select: none;
  font-size: 16px;

  // 防止 relative 子元素 margin 穿透
  &::before {
    content: ''
    display: block;
    overflow: hidden;
    height: 0;
    width: 1px;
  }

  &.active {
    // outline: 1px dashed #faad14;
    // box-shadow: inset rgba(102, 88, 88, 0.43) 0px 0px 10px 0px;
  }

  &.dragentered {
    outline: 1px dashed #faad14;
    box-shadow: rgba(102, 88, 88, 0.43) 0px 0px 10px 3px;
  }

  &.mouseovered {
    outline: 1px dotted #faad14;
    // box-shadow: rgba(102, 88, 88, 0.43) 0px 0px 10px 3px;
  }

  .bt {
    z-index: 0;
    left: 0;
    bottom: 0;
    font-size: 12px;
    color: #dddddd;
  }

  > .node-divider {
    height: 10px;
    position: absolute;
    left: 0;
    top: -5px;

    &.hover {
      background: rgba(255, 0, 0, 0.5);
    }
  }

  > .type-icon {
    position: absolute;
    right: 2px;
    top: 2px;
    width: 10px;
    height: 10px;
    font-size: 10px;
    color: #777;
  }
}
</style>
